<?php

namespace pmvc\annotation;

use pmvc\util\ClassUtil;
use pmvc\util\Stack;

use Exception;
use ReflectionClass;
use stdClass;

/**
 * A Lexer for parsing docblock style annotations.
 *
 */
class AnnotationParser {

	private $lexer;

	/**
	 * Creates the {@link AnnotationLexer}.
	 * @param string $data
	 */
	public function __construct($data) {
		$this->lexer = new AnnotationLexer($data);
	}

	/**
	 * Returns the next Annotation config.
	 */
	public function nextAnnotation() {
		$depth = 0;

		// get the next token
		$token = $this->lexer->nextToken();
		if ($token===false) {
			return false;
		}

		// make sure it's an annotation class
		if ($token->type!=AnnotationLexer::TOKEN_ANNOTATION_CLASS) {
			throw new Exception("Invalid token, expected annotation class: ".print_r($token, true));
		}

		// create it
		$current = new stdClass();
		$current->name	= $token->value;
		$current->vars	= Array();

		// create annotation stack
		$annotationStack 	= new Stack();
		$varNameStack		= new Stack();
		$arrayStack			= new Stack();

		// parse the rest of the tokens
		while (($token=$this->lexer->nextToken())!=false) {
			switch($token->type) {

				// new annotation
				case AnnotationLexer::TOKEN_ANNOTATION_CLASS: {
					// crate a new annotation
					$newAnnotation = new stdClass();
					$newAnnotation->name 	= $token->value;
					$newAnnotation->vars	= Array();
					
					// put the current one on the stack
					$annotationStack->pushRef($current);
					$arrayStack->push(false);

					// make the new one current
					$current = $newAnnotation;
					break;
				}

				// done with that annotation
				case AnnotationLexer::TOKEN_ANNOTATION_FINISHED: {
					if ($annotationStack->size()>0) {
						$oldCurrent = $current;
						$current = $annotationStack->popRef();
						$arrayStack->pop();
						if ($arrayStack->top()===false) {
							$this->setVarValue($current, $varNameStack->pop(), $oldCurrent);
						} else {
							array_push($arrayStack->topRef(), $oldCurrent);
						}
						break;
					} else {
						return $current;
					}
				}

				// name of a variable
				case AnnotationLexer::TOKEN_VAR_NAME: {
					$varNameStack->push($token->value);
					break;
				}

				// array start
				case AnnotationLexer::TOKEN_VAR_ARRAY_START: {
					$arrayStack->push(Array());
					break;
				}

				// array end
				case AnnotationLexer::TOKEN_VAR_ARRAY_END: {
					$array = $arrayStack->popRef();
					if ($arrayStack->top()===false) {
						$this->setVarValue($current, $varNameStack->pop(), $array);
					} else {
						array_push($arrayStack->topRef(), $array);
					}
					break;
				}

				// string variable
				case AnnotationLexer::TOKEN_VAR_STRING: {
					if ($arrayStack->top()===false) {
						$this->setVarValue($current, $varNameStack->pop(), $this->parseStringVar($token));
					} else {
						array_push($arrayStack->topRef(), $this->parseStringVar($token));
					}
					break;
				}

				// bool variable
				case AnnotationLexer::TOKEN_VAR_BOOL: {
					if ($arrayStack->top()===false) {
						$this->setVarValue($current, $varNameStack->pop(), $this->parseBoolVar($token));
					} else {
						array_push($arrayStack->topRef(), $this->parseBoolVar($token));
					}
					break;
				}

				// float variable
				case AnnotationLexer::TOKEN_VAR_FLOAT: {
					if ($arrayStack->top()===false) {
						$this->setVarValue($current, $varNameStack->pop(), $this->parseFloatVar($token));
					} else {
						array_push($arrayStack->topRef(), $this->parseFloatVar($token));
					}
					break;
				}

				// int variable
				case AnnotationLexer::TOKEN_VAR_INT: {
					if ($arrayStack->top()===false) {
						$this->setVarValue($current, $varNameStack->pop(), $this->parseIntVar($token));
					} else {
						array_push($arrayStack->topRef(), $this->parseIntVar($token));
					}
					break;
				}

				// constant
				case AnnotationLexer::TOKEN_VAR_CONST: {
					if ($arrayStack->top()===false) {
						$this->setVarValue($current, $varNameStack->pop(), $this->parseConstValue($token, $current));
					} else {
						array_push($arrayStack->topRef(), $this->parseConstValue($token, $current));
					}
					break;
				}

				// null variable
				case AnnotationLexer::TOKEN_VAR_NULL: {
					if ($arrayStack->top()===false) {
						$this->setVarValue($current, $varNameStack->pop(), null);
					} else {
						array_push($arrayStack->topRef(), null);
					}
					break;
				}

				// unknown
				default: {
					throw new Exception("Unknown token: ".print_r($token, true));
				}
			}
		}

		// should never get here
		throw new Exception("Unexpected end of token stream");
	}

	/**
	 * Sets the variable value maintaining the stack.
	 * @param annotation $annotation
	 * @param array $varNameStack
	 * @param mixed $value
	 * @throws Exception on error
	 */
	private function setVarValue(&$annotation, $varName, $value) {
		if (array_key_exists($varName, $annotation->vars)) {
			throw new Exception("Variable ".$varName." already defined for ".$annotation->name);
		} else {
			$annotation->vars[$varName] = $value;
		}
	}

	/**
	 * @param stdClass $token the token
	 * @return string
	 */
	private function parseStringVar(stdClass $token) {
		$ret = "";
		eval("\$ret=".$token->value.";");
		return $ret;
	}

	/**
	 * @param stdClass $token the token
	 * @return bool
	 */
	private function parseBoolVar(stdClass $token) {
		return strtolower($token->value)=="true";
	}

	/**
	 * @param stdClass $token the token
	 * @return float
	 */
	private function parseFloatVar(stdClass $token) {
		return floatval($token->value);
	}

	/**
	 * @param stdClass $token the token
	 * @return int
	 */
	private function parseIntVar(stdClass $token) {
		return intval($token->value);
	}

	/**
	 * @param stdClass $token
	 * @param stdClass $annotation
	 */
	private function parseConstValue(stdClass $token, &$annotation) {
		list($className, $const) = explode("::", $token->value);
		if (strtolower($className)=="self") {
			$className = $annotation->name;
		}
		if (!class_exists($className, true) && substr($className, 0, 1)!="\\") {
			$className = "\\".$className;
		}
		$clazz = new ReflectionClass($className);
		return $clazz->getConstant($const);
	}
}

?>